/*
	jGrouse framework
	Copyright 2007 by Denis Riabtchik 
	All rights reserved.
	
	Licensed under modified BSD license. See license.txt for details
*/

/**
	Contains classes encapsulating browser-specific functionality
	@module jgrouse.browser.Browser
	@requires module-jgrouse.base
*/
jgrouse.module(
{
	name : 'jgrouse.browser.Browser',
	requires : ['jgrouse.base'],
	body : function(embed)
	{
		eval(embed);
		
		/**
			Base class for all browsers
			@class jgrouse.browser.BaseBrowser
			@extends jgrouse.BaseObject
		*/
		jgrouse.define('jgrouse.browser.BaseBrowser', 'jgrouse.BaseObject', function(salt)
		{
			eval(salt);
			
			return  {
				/**
					@constructor initialize
				*/
				initialize : function()
				{
					_super(this, 'initialize');
					/**
					 * Mappings of event names to browser-specific event names. 
					 * Key is public event name, value is browser-specific event name 
					 * @variable _eventNameMap 
					 */
					this._eventNameMap = {'trueKeypress' : 'keypress'};
					
					/**
					 * Mappings of CSS properties to members of style structure. 
					 * Key is CSS property name, value is corresponding member name
					 * @variable _cssMap
					 */
					this._cssMap = {'float' : 'cssFloat'};
					
					/**
					 * History tracker for the browser
					 * @variable {jgrouse.browser.HistoryTracker} _historyTracker
					 */
					this._historyTracker = null;
					
				},
				/**
					Map generic event name into browser-specific event name.
					@function {String} mapEventName
					@param {String} eventName - name of generic event name
					@returns {String} browser-specific event name 
				*/
				mapEventName : function(eventName)
				{
					var n = this._eventNameMap[eventName];
					return n? n : eventName;
				},
				
				/**
					Map css property name to browser-specific property name. 
					Used to map property names from CSS standard into names that could be used in scripts
					@function {String} mapCssProperty
					@param {String} property - name of generic css property
					@returns {String} name of mapped property
				*/
				mapCssProperty : function(property)
				{
					var v = this._cssMap[property];
					return v? v : property;
				},
				
				/**
				 * @ifunction jgrouse.browser.domEventHandler
				 * @param {DOMEvent} event that is being handled
				 */
				
				/**
					Add event listener to DOM element
					@function addListener
					@param {DOMElement} dom - element to which the listener should be attached
					@param {String} eventName - name of event being observed
					@param {domEventHandler} listener callback that should be called when event is triggered
					@param {boolean} doCapture indicates if it is necessary to use capture capabilities
				*/
				addListener : function(dom, eventName, listener, doCapture)
				{
					dom.addEventListener(this.mapEventName(eventName), listener, doCapture);
				},
				
				/**
					Remove event listener from DOM element
					@function removeListener
					@param {DOMElement} dom - element to which the listener was attached
					@param {String} eventName - name of event being observed
					@param {domEventHandler} listener callback that was used
					@param {boolean} doCapture indicates if it was necessary to use capture capabilities
					
				*/
				removeListener : function(dom, eventName, listener, doCapture)
				{
					dom.removeEventListener(this.mapEventName(eventName), listener, doCapture);
				},
				
				/**
					Get attribute of DOM node. 
					@function {String} getAttribute
					@param {DOMElement} domNode 
					@param {String} attrName
					@returns value of retrieved attribute
				*/
				getAttribute : function(domNode, attrName)
				{
					return domNode.getAttribute(attrName);
				},
				
				/**
					Get all specified attributes pf DOM node.
					@function {DOMAttribute[]} getAttributes
					@param {DOMElement} domNode
					@returns array of DOMAttribute[] objects
				*/
				getAttributes : function(domNode)
				{
					var attrs = domNode.attributes;
					var res = [];
					for (var i = attrs.length - 1; i >= 0; i--)
					{
						var a = attrs.item(i);
						if (a.specified)
						{
							res.push(a);
						}
					}
					return res;
				},
				
				/**
					Get style computed on element's class, style and inherited styles.
					@function {String} getComputedStyle
					@param {DOMElement} element
					@param {String} styleName
					@returns value of retrieved computed style
				*/
				getComputedStyle : function(element, styleName)
				{
					var s = this.mapCssProperty(styleName);
					return document.defaultView.getComputedStyle(element, null)[s];
				},
				
				/**
					Remove all attributes from element.
					@function clearAttributes
					@param {DOMElement} element
				*/
				clearAttributes : function(element)
				{
					var attrs = element.attributes;
					var attrNames = [];
					for (var i = attrs.length - 1; i >= 0; i--)
					{
						attrNames.push(attrs.item(i).nodeName);
					}
					jgrouse.forEach(attrNames, function(itemName)
					{
						attrs.removeNamedItem(itemName);
					});
				},
				
				/**
					Cancel DOM event
					@function cancelEvent
					@param {DOMEvent} event - event to be cancelled
				*/
				cancelEvent : function(event)
				{
					event.preventDefault();
					event.stopPropagation();
				},
				
				/**
				 * Initialize history tracker.
				 * @function initHistoryTracker
				 */
				initHistoryTracker : function()
				{
					this._historyTracker = new jgrouse.browser.TimerHistoryTracker(200);
				},
				
				/**
				 * Fire notification regarding the detected change in current URL
				 * @function fireHistoryChange
				 */
				fireHistoryChange : function()
				{
					if (this._historyTracker)
					{
						this._historyTracker.fireHistoryChange();
					}
				},
				
				/**
				 * Add item to browser history. The item would be added as "hash" to the current URL 
				 * @function setHistoryEntry
				 * @param {String} entry - item to be set as hash in window's URL 
				 */
				setHistoryEntry : function(entry)
				{
					if (this._historyTracker)
					{
						this._historyTracker.setHistoryEntry(entry);
					}
				},
				
				/**
				 * Normalize ancor. Used internally.
				 * @function {String} normalizeAncor
				 * @param {String} ancor - URL containing ancor
				 */
				normalizeAncor : function(ancor)
				{
					return ancor;
				},
				
				/**
				Retrieve CSS styles from DOM node
				@function {object} getStyles
				@param {DOMElement} element - target node
				@returns object containing name/value pairs for CSS styles. Names are camelized and 
				could be browser-specific.
				*/			
				getStyles : function(element)
				{
					var res = {};
					for (var i in element.style)
					{
						var v = element.style[i];
						if (i != 'cssText' && i != 'length' && v && typeof v != 'function')
						{
							res[i] = element.style[i];
						}					
					}
					return res;
				},
				
				/**
				 * Scroll element to view
				 * @function ?
				 * @param {DOMElement} domElement - element to scroll into view
				 * @param {String} align - 'left', 'top', null
				 */
				scrollIntoView : function(domElement, align)
				{
					domElement.scrollIntoView(true);
				},
				
				/**
				 * Internal function - ensures that when node.scrollIntoView is called, that call would affect only
				 * scrollLeft and scrollTop of the first parent that allows scrolling, thus restoring those values 
				 * for nodes that have attribute overflow set to 'hidden' or 'visible'
				 * @function {private} _internalScrollIntoView
				 * @param {DOMElement} domElement - element to scroll into view
				 * @param {String} align - 'left', 'top', null
				 */
				_internalScrollIntoView : function(domElement, align)
				{
					var scrolls = [];
					var node = domElement.parentNode;
					var found = false;
					while (node != document)
					{
						var x = found? node.scrollLeft : null  ;
						var y = found? node.scrollTop : null;
						if (x === undefined)
						{
							x = 0;
						}
						if (y === undefined)
						{
							y = 0;
						}
						if (!found)
						{
							var o = this.getComputedStyle(node, 'overflow');
							found = o == 'auto' || o == 'scroll';
						}
						l_debug(node.nodeName + ' found = ' + found + ' x=' + x + ' y=' + y);
						scrolls.push({x:x, y:y});
						node = node.parentNode;
					}
					domElement.scrollIntoView(true);
					node = domElement.parentNode;
					while (node != document)
					{
						var v = scrolls[0];
						scrolls.splice(0, 1);
						if (align === 'top')
						{
							v.y = 0;
						} else 
						if (align === 'left')
						{
							v.x = 0;
						}
						
						if (v.x !== null)
						{
							node.scrollLeft = v.x;
						}
						if (v.y !== null)
						{
							node.scrollTop = v.y;
						}
						l_debug(node.nodeName + ' x=' + v.x + ' y=' + v.y + ' left=' + node.scrollLeft 
							+ ' top=' + node.scrollTop);
						node = node.parentNode;
					}					
				}
			};
		});
		
		/**
			Support for Mozilla browsers
			@class jgrouse.browser.Mozilla
			@extends BaseBrowser
		*/
		jgrouse.define('jgrouse.browser.Mozilla', 'jgrouse.browser.BaseBrowser', function(salt)
		{
			eval (salt);
			
			
			return {
				/**
					@constructor initialize
				*/
				initialize : function()
				{
					_super(this, 'initialize');
					l_info('mozilla!');
				}
				
			};
		});
		
		/**
			Support for MS Explorer
			@class jgrouse.browser.Explorer
			@extends jgrouse.browser.BaseBrowser
		*/
		jgrouse.define('jgrouse.browser.Explorer', 'jgrouse.browser.BaseBrowser', function(salt)
		{
			eval(salt);
			
			return {
				/**
					@constructor initialize
				*/
				initialize : function()
				{
					_super(this, 'initialize');
					this._cssMap['float'] = 'styleFloat';
				},
				
				/**
					@function addListener
					@inheritdesc
				*/
				addListener : function(dom, eventName, listener, doCapture)
				{
					var name = this.mapEventName(eventName);
					dom.attachEvent('on' + name, listener);
				},
				
				/**
					@function removeListener
					@inheritdesc
				*/
				removeListener : function(dom, eventName, listener, doCapture)
				{
					var name = this.mapEventName(eventName);
					dom.detachEvent('on' + name, listener);
				},
				

				/**
					@function getAttribute
					@inheritdesc
					
				*/				
				getAttribute : function(domNode, attrName)
				{
					return domNode.getAttribute(attrName, 2); // returns value exactly as it was set in script or source doc
				},
				
				/**
					@function getAttributes
					@inheritdesc
				*/
				getAttributes : function(domNode)
				{
					var res = _sup(this, arguments);
					if (domNode.tagName.toLowerCase() == 'input')
					{
						var a = domNode.attributes['value'];
						if (!a.specified)
						{
							res.push(a);
						}
						var t = domNode.attributes['type'];
						if (!t.specified)
						{
							res.push(t);
						}
					}
					else if (domNode.tagName.toLowerCase() == 'a')
					{
						var a = domNode.attributes['href'];
						if (a.specified)
						{
							a.value = domNode.getAttribute('href', 2);
						}
					}
				return res;
				},

				/**
					@function getComputedStyle
					@inheritdesc
				*/				
				getComputedStyle : function(element, styleName)
				{
					return element.currentStyle[this.mapCssProperty(styleName)];
				},

				/**
					@function clearAttributes
					@inheritdesc
				*/				
				clearAttributes : function(element)
				{
					element.clearAttributes();
				},
				
				/**
				 * @function cancelEvent
				 * @inheritdesc
				 */
				cancelEvent : function(event)
				{
					event.returnValue = null;
					event.cancelBubble = true;
				},

				/**
				 * @function initHistoryTracker
				 * @inheritdesc
				 */
				initHistoryTracker : function()
				{
					if (!this._historyTracker)
					{
						this._historyTracker = new jgrouse.browser.IFrameHistoryTracker(false);
					}
				},
				
				/**
				 * Normalize ancor. In IE the browser would try to convert all relative URLs into absolute, 
				 * based on URL of current window.
				 * This method tries to extract the original ancor from full URL, if possible
				 * @function {String} normalizeAncor
				 * @param {String} ancor
				 * @return extracted ancor or the original parameter if the passed value does not appear to 
				 * be a ancor-containing URL for the current window
				 */
				normalizeAncor : function(ancor)
				{
					var a = ancor.split('#');
					if (a.length == 1)
					{
						return ancor;
					}
					var href = window.location.href;
					var ha = href.split('#');
					if (ha[0] == a[0])
					{
						return '#' + a[1];
					}
					return ancor;
				},
				
				/**
				 * Scroll to view is not properly supported in Explorer, it screws up the view royally.
				 * So essentially in that case we are ensuring that offsetParent's scrollLeft and scrollTop
				 * are adjusted in order to show the element in question.
				 * Note - usage of _internalScrollToView might cause unpleasant flicker, so it is not used.
				 * @function ?
				 * @param {DOMElement} domElement
				 * @param align - 'left', 'top', null
				 */
				scrollIntoView : function(domElement, align)
				{
					var p = domElement.offsetParent;
					p.scrollLeft = domElement.offsetLeft;
					p.scrollTop = domElement.offsetTop;
				}
			}
		});
		
		/**
			Support for KHTML-based (Konqueror|Safari|KHTML) browsers
			@class jgrouse.browser.KHTML
			@extends jgrouse.browser.BaseBrowser
		*/		
		jgrouse.define('jgrouse.browser.KHTML', 'jgrouse.browser.BaseBrowser', function(salt)
		{
			eval(salt);
			
			return {
				/**
					@constructor initialize
				*/
				initialize : function()
				{
					_super(this, 'initialize');
					this._eventNameMap['keypress'] = 'keydown';
				},
				
				/**
				 * @function initHistoryTracker
				 * @inheritdesc
				 */
				initHistoryTracker : function()
				{
					this._historyTracker = new jgrouse.browser.IFrameHistoryTracker(true);
				},
				
				/**
				 * @function getStyles
				 * @inheritdesc
				 */
				getStyles : function(element)
				{
					var res = {};
					for (var i = 0; i < element.style.length; i++)
					{
						var sn = element.style.item(i);
						res[sn] = element.style[sn];
					} 
					return res;
				}
				
				
			}
		});
		
		/**
			Support for Opera browser
			@class jgrouse.browser.Opera
			@extends jgrouse.browser.BaseBrowser
		*/
		jgrouse.define('jgrouse.browser.Opera', 'jgrouse.browser.BaseBrowser', function(salt)
		{
			eval(salt);
			return {
				/**
					@constructor initialize
				*/
				initialize : function()
				{
					_super(this, 'initialize');
				},

				/**
					@function addListener
					@inheritdesc
					
				*/
				addListener : function(dom, eventName, listener, doCapture)
				{
					var name = this.mapEventName(eventName);
					dom.attachEvent('on' + name, listener);
				},

				/**
					@function removeListener
					@inheritdesc
				*/
				
				removeListener : function(dom, eventName, listener, doCapture)
				{
					var name = this.mapEventName(eventName);
					dom.detachEvent('on' + name, listener);
				},

				/**
				 * @function initHistoryTracker
				 * @inheritdesc
				 */
				initHistoryTracker : function()
				{
					this._historyTracker = new jgrouse.browser.IFrameHistoryTracker(true);
				},
				
				/**
				 * Scroll to view is not properly supported in Opera, it screws up the view royally
				 * @function ?
				 * @param {DOMElement} domElement
				 * @param align - 'left', 'top', null
				 */
				scrollIntoView : function(domElement, align)
				{
					this._internalScrollIntoView(domElement, align);
					/*
					var scrolls = [];
					var node = domElement.parentNode;
					var found = false;
					while (node)
					{
						var x = found? node.scrollLeft : null  ;
						var y = found? node.scrollTop : null;
						if (x === undefined)
						{
							x = 0;
						}
						if (y === undefined)
						{
							y = 0;
						}
						if (!found)
						{
							var o = this.getComputedStyle(node, 'overflow');
							found = o == 'auto' || o == 'scroll';
						}
						l_debug(node.nodeName + ' found = ' + found + ' x=' + x + ' y=' + y);
						scrolls.push({x:x, y:y});
						node = node.parentNode;
					}
					domElement.scrollIntoView(true);
					node = domElement.parentNode;
					while (node)
					{
						var v = scrolls[0];
						scrolls.splice(0, 1);
						if (align === 'top')
						{
							v.y = 0;
						} else 
						if (align === 'left')
						{
							v.x = 0;
						}
						
						if (v.x !== null)
						{
							node.scrollLeft = v.x;
						}
						if (v.y !== null)
						{
							node.scrollTop = v.y;
						}
						l_debug(node.nodeName + ' x=' + v.x + ' y=' + v.y + ' left=' + node.scrollLeft 
							+ ' top=' + node.scrollTop);
						node = node.parentNode;
					}*/
				}
			}
		});		
		
		
	},
	
	postInit : function(embed)
	{
		eval(embed);
		
		
		function getBrowserClass()
		{
			var userAgent = navigator.userAgent;
			var appName = navigator.appName;
			if (appName.indexOf('Microsoft') >= 0 && userAgent.indexOf('MSIE') >= 0)
			{
				return 'jgrouse.browser.Explorer';
			}
			if (userAgent.match(/Konqueror|Safari|KHTML/))
			{
				return 'jgrouse.browser.KHTML';
			}
			if (userAgent.indexOf('Gecko') >= 0)
			{
				return 'jgrouse.browser.Mozilla';
			}
			if (userAgent.indexOf('Opera') >= 0)
			{
				return 'jgrouse.browser.Opera'
			}
			return 'jgrouse.browser.BaseBrowser';
		}

		var bn = getBrowserClass();
		var bc = jgrouse.resolveName(bn);
		jgrouse.browser.instance = new bc();

		jgrouse.require('jgrouse.browser.HistoryTracker', function()
		{
			jgrouse.browser.instance.initHistoryTracker();
		});
		
	}
});
